
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, const char* argv[])
{
    int exit_code = EXIT_FAILURE;
    const char* port = "2533"; /* default port number */
    const char* host = "localhost"; /* default ip address */
    struct addrinfo hints;
    struct addrinfo *addr, *addr_ptr;
    int sock;
    FILE* sock_file;
    char greeting[4096] = { 0 };

    switch(argc)
    {
    case 4:
        port = argv[3];
        /* no break */

    case 3:
        host = argv[2];
        /* no break */

    case 2:
        if(chdir(argv[1]) < 0) /* change working path to requested dir */
        {
            perror(argv[1]);
            exit(EXIT_FAILURE);
        }
        break;

    default:
        fprintf(stderr, "usage: %s dir [host [port]]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    if(getaddrinfo(host, port, NULL, &addr))
    {
        perror("getaddrinfo");
        exit(EXIT_FAILURE);
    }

    for(addr_ptr = addr; addr_ptr != NULL; addr_ptr = addr_ptr->ai_next)
    {
        if((sock = socket(addr_ptr->ai_family, addr_ptr->ai_socktype, addr_ptr->ai_protocol)) >= 0)
        {
            if(connect(sock, addr_ptr->ai_addr, addr_ptr->ai_addrlen) >= 0)
            {
                break;
            }

            close(sock);
        }
    }

    freeaddrinfo(addr);

    if(addr_ptr == NULL)
    {
        fprintf(stderr, "unable to connect to %s:%s\n", host, port);
        exit(EXIT_FAILURE);
    }

    sock_file = fdopen(sock, "a+");

    /* greeting from server */
    if(!fgets(greeting, sizeof(greeting), sock_file))
    {
        if(ferror(sock_file))
        {
            perror("fgets");
        }

        goto end_session;
    }

    fputs(greeting, stdout);

    fseek(sock_file, 0, SEEK_CUR); /* Force change of stream direction */

    while(1)
    {
        char request[4096] = { 0 };
        char* method;
        char* args;

        /* request */
        fgets(request, sizeof(request), stdin);
        method = strtok(request, " \t\r\n");
        args = strtok(NULL, " \t\r\n");

        if(!method)
        {
            fputs("invalid request: <empty>\n", stderr);
        }
        else if(strcmp(method, "DIR") == 0)
        {
            if(args != NULL)
            {
                fprintf(stderr, "%s: invalid arguments\n", method);
            }
            else
            {
                fputs("DIR\r\n\r\n",sock_file);

                fseek(sock_file, 0, SEEK_CUR); /* Force change of stream direction */

                while(1)
                {
                    char header[4096] = { 0 };

                    if(!fgets(header, sizeof(header), sock_file))
                    {
                        if(ferror(sock_file))
                        {
                            perror("fgets");
                        }

                        goto end_session;
                    }
                    else if(strlen(header) == 0 || (strlen(header) == 1 && header[0] == '\r') || (strlen(header) == 2 && header[0] == '\r' && header[1] == '\n'))
                    {
                        /* empty header indicates end of headers */
                        break;
                    }

                    /* handling List-Files header */
                    if(strncmp(header, "List-Files:", 11) == 0)
                    {
                        int i = 11, j;
                        char file_name[4096];

                        while(sscanf(&header[i], "%*[ \t]\"%[^\"]\"%n", file_name, &j) == 1)
                        {
                            fprintf(stdout, "%s\n", file_name);
                            i += j;
                        }
                    }
                }
            }
        }
        else if(strcmp(method, "GET") == 0)
        {
            FILE* file;

            if(args == NULL)
            {
                fprintf(stderr, "%s: missing arguments\n", method);
            }
            else if((file = fopen(args, "wb")) == NULL)
            {
                perror(args);
            }
            else
            {
                uint32_t content_length = 0;

                fprintf(sock_file,"GET %s\r\n\r\n",args);

                fseek(sock_file, 0, SEEK_CUR); /* Force change of stream direction */

                /* parsing relevant headers */
                while(1)
                {
                    char header[4096] = { 0 };

                    if(!fgets(header, sizeof(header), sock_file))
                    {
                        if(ferror(sock_file))
                        {
                            perror("fgets");
                        }

                        goto end_session;
                    }
                    else if(strlen(header) == 0 || (strlen(header) == 1 && header[0] == '\r') || (strlen(header) == 2 && header[0] == '\r' && header[1] == '\n'))
                    {
                        /* empty header indicates end of headers */
                        break;
                    }

                    /* handling Content-Length header */
                    sscanf(header, "Content-Length: %ud", &content_length);
                }

                /* downloading file content */
                while(content_length > 0)
                {
                    char buffer[4096];
                    size_t len;

                    if((len = fread(buffer, 1, MIN(sizeof(buffer), content_length), sock_file)) == 0)
                    {
                        if(ferror(sock_file))
                        {
                            perror("fread");
                        }

                        goto end_session;
                    }
                    else if(fwrite(buffer, 1, len, file) != len)
                    {
                        perror("fwrite");
                        goto end_session;
                    }

                    content_length -= len;
                }

                fclose(file);

                fputs("transfer complete\n", stdout);
            }
        }
        else if(strcmp(method, "PUT") == 0)
        {
            FILE* file;

            if(args == NULL)
            {
                fprintf(stderr, "%s: missing arguments\n", method);
            }
            else if((file = fopen(args, "rb")) == NULL)
            {
                perror(args);
            }
            else
            {
                long content_length;

                /* reading file size */
                fseek(file, 0, SEEK_END);
                content_length = ftell(file);
                fseek(file, 0, SEEK_SET);

                /* sending command and relevant headers */
                fprintf(sock_file, "PUT %s\r\nContent-Length: %ld\r\n\r\n", args, content_length);

                /* uploading file content */
                while(content_length > 0)
                {
                    char buffer[4096];
                    size_t len;

                    if((len = fread(buffer, 1, MIN(sizeof(buffer), content_length), file)) == 0)
                    {
                        if(ferror(file))
                        {
                            perror("fread");
                        }

                        goto end_session;
                    }
                    else if(fwrite(buffer, 1, len, sock_file) != len)
                    {
                        perror("fwrite");
                        goto end_session;
                    }

                    content_length -= len;
                }

                fclose(file);

                fputs("transfer complete\n", stdout);
            }
        }
        else if(strcmp(method, "QUIT") == 0)
        {
            exit_code = 0;
            break;
        }
        else
        {
            fprintf(stderr, "invalid request: %s\n", method);
        }

        fseek(sock_file, 0, SEEK_CUR); /* Force change of stream direction */
    }

end_session:

    fclose(sock_file);
    close(sock);

    return exit_code;
}

